package com.pithypan.service.impl;

import com.pithypan.common.*;
import com.pithypan.common.exceptionHandler.MyException;
import com.pithypan.common.utils.*;
import com.pithypan.dto.SessionWebUserDto;
import com.pithypan.dto.UploadResultDto;
import com.pithypan.dto.UserSpaceDto;
import com.pithypan.entity.FileInfo;
import com.pithypan.entity.UserInfo;
import com.pithypan.mapper.FileInfoMapper;
import com.pithypan.mapper.UserInfoMapper;
import com.pithypan.service.FileInfoService;
import com.pithypan.vo.FileInfoQuery;
import com.pithypan.vo.PaginationResultVo;
import io.netty.util.Constant;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * <p>
 * 文件信息 服务实现类
 * </p>
 *
 * @author author
 * @since 2023-11-23
 */
@Service
@Transactional
public class FileInfoServiceImpl implements FileInfoService {

    @Resource
    private FileInfoMapper fileInfoMapper;
    @Resource
    private UserInfoMapper userInfoMapper;
    @Resource
    @Lazy     //解决循环依赖
    private FileInfoServiceImpl fileInfoService;     //uploadFile方法需求
    @Resource
    private RedisUtils redisUtils;
    @Value("${project.folder}")
    private String projectFolder;      //存储文件的路径

    private static final Logger logger = LoggerFactory.getLogger(FileInfoServiceImpl.class);


    /**
     * 分页查询文件
     * @param query
     * @return
     */
    @Override
    public PaginationResultVo findListByPage(FileInfoQuery query) {
        PaginationResultVo resultVo = new PaginationResultVo();
        resultVo.setPageSize(query.getPageSize());      //每页显示数据项数
        resultVo.setPageNo(query.getPageNo());          //页码

        //计算总记录数
        int totalCount = fileInfoMapper.findTotalCount(query);
        resultVo.setTotalCount(totalCount);

        //查询数据集
        int start = (query.getPageNo() - 1) * query.getPageSize();  //计算起始位置
        query.setStart(start);
        ArrayList<FileInfo> list = fileInfoMapper.findListByPage(query);
        resultVo.setList(list);

        //计算总页数
        int pageTotal = totalCount % query.getPageSize() == 0 ?
                totalCount /query.getPageSize() : (totalCount / query.getPageSize()) + 1;
        resultVo.setPageTotal(pageTotal);

        return resultVo;
    }


    /**
     * 上传文件
     * @param webUserDto 登陆用户信息
     * @param fileId     文件id
     * @param file       文件的分片
     * @param fileName   文件名
     * @param filePid    文件父级目录id
     * @param fileMd5    文件Md5值
     * @param chunkIndex 当前上传的是第几分片
     * @param chunks     文件总分片
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UploadResultDto uploadFile(SessionWebUserDto webUserDto, String fileId, MultipartFile file, String fileName, String filePid, String fileMd5, Integer chunkIndex, Integer chunks) {
        Boolean uploadSuccess = false;          //出现异常状态，是否删除暂存目录内的所有分片
        File tempFileFolder = null;             //暂存临时目录路径
        String userId = webUserDto.getUserId();
        //从redis中查询用户的内存空间信息
        UserSpaceDto userSpaceDto = (UserSpaceDto) redisUtils.get(Constants.REDIS_KEY_USER_SPACE_USE + userId);
        //判断成立则证明该分片为文件第一个分片，为其生成id
        if (StringTools.isEmpty(fileId)) {
            fileId = StringTools.getRandomString(10);
        }
        //创建返回前端
        UploadResultDto resultDto = new UploadResultDto();
        resultDto.setFileId(fileId);     //设置返回文件id
        try {
            //chunkIndex == 0 证明此次上传的是文件的第一个分片
            if (chunkIndex == 0) {
                //根据Md5值和文件状态查询文件
                List<FileInfo> list = fileInfoMapper.findListByMd5AndStatus(fileMd5, FileStatusEnums.USING.getStatus());

                /*--------------------------------*/
                //查询到文件，则可以进行秒传
                /*--------------------------------*/
                if (!list.isEmpty()) {
                    FileInfo dbFile = list.get(0);
                    //判断用户网盘空间是否充足
                    if (dbFile.getFileSize() + userSpaceDto.getUseSpace() > userSpaceDto.getTotalSpace()) {
                        ResponseCodeEnum r = ResponseCodeEnum.CODE_904;
                        throw new MyException(r.getCode(), r.getMsg());
                    }

                    //数据复制（秒传）
                    dbFile.setFileId(fileId);
                    dbFile.setFilePid(filePid);
                    dbFile.setUserId(userId);
                    Date curDate = new Date();
                    dbFile.setCreateTime(curDate);
                    dbFile.setLastUpdateTime(curDate);
                    dbFile.setStatus(FileStatusEnums.USING.getStatus());  //文件的转码状态
                    dbFile.setDelFlag(FileDelFlagEnums.USING.getFlag());  //文件是否删除
                    dbFile.setFileMd5(fileMd5);                           //Md5
                    fileName = autoRename(filePid, userId, fileName);     //调用自定义方法判断是否重命名
                    dbFile.setFileName(fileName);                         //文件名
                    fileInfoMapper.insert(dbFile);                        //持久化

                    //调用自定义方法更新用户空间信息
                    updateUserSpace(webUserDto, dbFile.getFileSize());

                    resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode()); //返回文件上传状态

                    return resultDto;
                }
            }
            /*--------------------------------*/
            //正常上传
            /*--------------------------------*/
            //判断用户网盘空间是否充足
            Long currentTempSize = getFileTempSize(userId, fileId); //查询暂存目录内的分片总和大小
            if (currentTempSize + file.getSize() + userSpaceDto.getUseSpace() > userSpaceDto.getTotalSpace()) {
                ResponseCodeEnum r = ResponseCodeEnum.CODE_904;
                throw new MyException(r.getCode(), r.getMsg());
            }

            //暂存临时目录
            String tempFolderName = projectFolder + "/temp/";
            String currentUserFolderName = userId + fileId;
            tempFileFolder = new File(tempFolderName + currentUserFolderName);
            if (!tempFileFolder.exists()) {
                tempFileFolder.mkdirs();  //目录不存在则创建
            }
            File newFile = new File(tempFileFolder.getPath() + "/" + chunkIndex);
            file.transferTo(newFile);     //transferTo：将MultipartFile对象中的文件内容写入到指定的文件或目录中
            saveFileTempSize(userId, fileId, file.getSize());  //自定义方法：更新 暂存目录内的分片总和大小

            //最后一个分片上传之前
            if (chunkIndex < chunks - 1) {
                resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
                return resultDto;
            }

            //最后一个分片上传完成，持久化，异步合并临时目录里的分片
            saveFileTempSize(userId, fileId, file.getSize());         //自定义方法：更新 暂存目录内的分片总和大小
            String month = DateUtil.toYYYYMM(new Date());             //将date类型转换成yyyymm格式
            String fileSuffix = StringTools.getFileSuffix(fileName);  //获得后缀名
            String realFileName = currentUserFolderName + fileSuffix; //目录中的真实文件名
            FileTypeEnums fileTypeEnums = FileTypeEnums.getFileTypeBySuffix(fileSuffix); //根据后缀名匹配文件类型
            fileName = this.autoRename(filePid, userId, fileName);    //调用自定义方法判断是否重命名

            //创建持久化
            FileInfo fileInfo = new FileInfo();
            fileInfo.setFileId(fileId);
            fileInfo.setUserId(userId);
            fileInfo.setFileMd5(fileMd5);
            fileInfo.setFileName(fileName);
            fileInfo.setFilePath(month + "/" + realFileName);         //文件路径
            fileInfo.setFilePid(filePid);                             //父级目录Id
            Date curDate = new Date();
            fileInfo.setCreateTime(curDate);
            fileInfo.setLastUpdateTime(curDate);
            fileInfo.setFileCategory(fileTypeEnums.getCategory().getCategory()); //文件大分类
            fileInfo.setFileType(fileTypeEnums.getType());             //文件细分类
            fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus());  //文件转码状态
            fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType());//代表是文件或目录
            fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());     //文件删除状态
            this.fileInfoMapper.insert(fileInfo);                      //持久化

            Long totalSize = this.getFileTempSize(userId, fileId);     //查询暂存目录内的分片总和大小，此时因为所有分片已上传完成，也可以代表文件大小
            this.updateUserSpace(webUserDto, totalSize);               //更新用户网盘空间使用情况

            resultDto.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());  //返回前端

            //需要事务提交之后调用，所以采用异歩(fileInfoService需要交由Spring管理，直接调用transferFile并没有交由Spring管理)
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);  //合并文件
                }
            });
        }
        catch (MyException e) {
            logger.error("文件上传失败！！！", e);
            uploadSuccess = true;   //出现异常状态，是否删除暂存目录内的所有分片
            throw e;
        } catch (IOException e) {
            logger.error("文件上传失败！！！", e);
            uploadSuccess = true;
        } finally {
            if (uploadSuccess && tempFileFolder != null) {
                try {
                    FileUtils.deleteDirectory(tempFileFolder);  //删除目录
                } catch (IOException e) {
                    logger.error("删除暂存临时目录失败！！！", e);
                }
            }
        }

        return resultDto;
    }


    /**
     * 根据 文件id 和 用户id 查询文件
     * @param fileId
     * @param userId
     * @return
     */
    @Override
    public FileInfo getFileInfoByFileIdAndUserId(String fileId, String userId) {
        return fileInfoMapper.findByFileIdAndUserId(fileId, userId);
    }


    /**
     * 新建目录
     * @param userId         用户id
     * @param filePid        目录的父级id
     * @param foloderName    目录名称
     * @return
     */
    @Override
    public FileInfo newFoloder(String userId, String filePid, String foloderName) {
        //目录名称是否已存在
        checkFileName(userId, filePid, foloderName, FileFolderTypeEnums.FOLDER.getType());
        //持久化
        Date curDate = new Date();
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileId(StringTools.getRandomString(10));
        fileInfo.setUserId(userId);
        fileInfo.setFilePid(filePid);
        fileInfo.setFileName(foloderName);
        fileInfo.setFolderType(FileFolderTypeEnums.FOLDER.getType());
        fileInfo.setCreateTime(curDate);
        fileInfo.setLastUpdateTime(curDate);
        fileInfo.setStatus(FileStatusEnums.USING.getStatus());
        fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfoMapper.insert(fileInfo);
        return fileInfo;
    }


    /**
     * 修改文件名称
     * @param userId
     * @param fileId
     * @param fileName
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfo rename(String userId, String fileId, String fileName) {
        FileInfo fileInfo = this.fileInfoMapper.findByFileIdAndUserId(fileId, userId);
        if (fileInfo == null) {
            throw new MyException(905, "文件不存在");
        }

        String filePid = fileInfo.getFilePid();
        checkFileName(userId, filePid, fileName, fileInfo.getFolderType());   //文件名是否已存在
        if (FileFolderTypeEnums.FILE.getType().equals(fileInfo.getFolderType())) {    //判断是否为非目录类型
            fileName = fileName + StringTools.getFileSuffix(fileInfo.getFileName());  //前端是不传递后缀名的
        }
        //数据更新
        Date curDate = new Date();
        FileInfo dbInfo = new FileInfo();
        dbInfo.setFileName(fileName);
        dbInfo.setLastUpdateTime(curDate);
        this.fileInfoMapper.updateByFileIdAndUserId(userId, fileId, dbInfo);

        //再次判断是否有相同名称的文件
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setUserId(userId);
        fileInfoQuery.setFilePid(filePid);
        fileInfoQuery.setFileName(fileName);
        fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag());
        Integer count = this.fileInfoMapper.selectFileCount(fileInfoQuery);
        if (count > 1) {
            throw new MyException(599, "此目录下已存在同名文件，请修改名称");
        }
        //返回更新后的数据
        fileInfo.setFileName(fileName);
        fileInfo.setLastUpdateTime(curDate);
        return fileInfo;
    }


    /**
     * 根据条件查询文件
     * @param infoQuery
     * @return
     */
    @Override
    public List<FileInfo> findListByParam(FileInfoQuery infoQuery) {
        return fileInfoMapper.selectFileList(infoQuery);
    }


    /**
     * 移动文件
     * @param userId
     * @param fileIds   需要移动的文件id
     * @param filePid   移动目的地
     */
    @Override
    public void changeFileFolder(String userId, String fileIds, String filePid) {
        //校验参数
        if (fileIds.equals(filePid)) {
            throw new MyException(ResponseCodeEnum.CODE_605.getCode(), ResponseCodeEnum.CODE_605.getMsg());
        }
        //目的地不是根目录的情况，则判断目的地目录是否存在
        if (!Constants.ZERO_STR.equals(filePid)) {
            FileInfo fileInfo = this.getFileInfoByFileIdAndUserId(filePid, userId);
            if (fileInfo == null || !FileDelFlagEnums.USING.getFlag().equals(fileInfo.getDelFlag())) {
                throw new MyException(ResponseCodeEnum.CODE_605.getCode(), ResponseCodeEnum.CODE_605.getMsg());
            }
        }

        //查询目的地目录下的所有文件
        String[] fileIdArray = fileIds.split(",");
        FileInfoQuery infoQuery = new FileInfoQuery();
        infoQuery.setUserId(userId);
        infoQuery.setFilePid(filePid);
        List<FileInfo> dbFileList = fileInfoMapper.selectFileList(infoQuery);  //查找目的地目录下的所有文件
        //将 list 转换为以 fileName 为key的Map集合（如果有两个重名的情况选第二个）
        Map<String, FileInfo> dbFileNameMap = dbFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (data1, data2) -> data2));

        //查询需要移动的文件
        infoQuery = new FileInfoQuery();
        infoQuery.setUserId(userId);
        infoQuery.setFileIdArray(fileIdArray);
        List<FileInfo> selectFileList = fileInfoMapper.selectFileList(infoQuery);

        //判断 目的地目录中 是否有与 需移动文件 同名的文件，有则对需移动文件重命名
        for (FileInfo item : selectFileList) {
            FileInfo root = dbFileNameMap.get(item.getFileName());
            FileInfo updateInfo = new FileInfo();
            if (root != null) {
                String fileName = StringTools.rename(item.getFileName());  //重命名
                updateInfo.setFileName(fileName);
            }
            updateInfo.setFilePid(filePid);  //父级id
            fileInfoMapper.updateByFileIdAndUserId(userId, item.getFileId(), updateInfo);  //修改文件
        }
    }


    /**
     * 删除文件，修改delFlag（注：删除目录时，需要边目录下的所有文件也一并删除）
     * @param userId
     * @param fileIds  需删除文件的id串
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeFile2RecycleBatch(String userId, String fileIds) {
        //查询需要删除的文件
        String[] fileIdArray = fileIds.split(",");
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFileIdArray(fileIdArray);
        query.setDelFlag(FileDelFlagEnums.USING.getFlag());
        List<FileInfo> fileInfoList = this.fileInfoMapper.selectFileList(query);

        if (fileInfoList.isEmpty()) {
            return;     //未找到需要删除的文件
        }

        //调用自定义方法，递归收集需要删除的所有子目录
        ArrayList<String> delFilePidList = new ArrayList<>();
        for (FileInfo fileInfo : fileInfoList) {
            findAllSubFolderFileList(delFilePidList, userId, fileInfo.getFileId(), FileDelFlagEnums.USING.getFlag());
        }

        //将收集到的子目录进行删除（连同目录下的子文件）
        if (!delFilePidList.isEmpty()) {
            FileInfo updateInfo = new FileInfo();
            updateInfo.setDelFlag(FileDelFlagEnums.DEL.getFlag());
            this.fileInfoMapper.updateFileDelFlagBatch(updateInfo, userId, delFilePidList, null, FileDelFlagEnums.USING.getFlag());
        }

        //将前端选中的文件更新为回收站
        List<String> delFileIdList = Arrays.asList(fileIdArray);
        FileInfo fileInfo = new FileInfo();
        fileInfo.setRecoveryTime(new Date());
        fileInfo.setDelFlag(FileDelFlagEnums.RECYCLE.getFlag());
        this.fileInfoMapper.updateFileDelFlagBatch(fileInfo, userId, null, delFileIdList, FileDelFlagEnums.USING.getFlag());
    }


    /**
     * 从回收站中还原文件
     * @param userId
     * @param fileIds
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void recoverFileBatch(String userId, String fileIds) {
        String[] fileIdArray = fileIds.split(",");
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFileIdArray(fileIdArray);
        query.setDelFlag(FileDelFlagEnums.RECYCLE.getFlag());
        List<FileInfo> fileInfoList = this.fileInfoMapper.selectFileList(query); //查找待还原的文件

        //（查询时事务是不开启的，当涉及到update时则开启事务。所以不开启事务的逻辑统一放前面）
        //遍历集合，递归收集需要还原的所有子目录
        List<String> delFileSubFloderFileIdList = new ArrayList<>();
        for (FileInfo fileInfo : fileInfoList) {
            if (FileFolderTypeEnums.FOLDER.getType().equals(fileInfo.getFolderType())) {
                findAllSubFolderFileList(delFileSubFloderFileIdList, userId, fileInfo.getFileId(), FileDelFlagEnums.DEL.getFlag());
            }
        }

        //查询所有根目录下的文件
        query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFilePid(Constants.ZERO_STR);
        query.setDelFlag(FileDelFlagEnums.USING.getFlag());
        List<FileInfo> allRootFileList = this.fileInfoMapper.selectFileList(query);
        //将 list 转换为以 fileName 为key的Map集合（如果有两个重名的情况选第二个）
        Map<String, FileInfo> rootFileMap = allRootFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (data1, data2) -> data2));

        //将收集到的子目录修改为正常（连同目录下的子文件）
        if (!delFileSubFloderFileIdList.isEmpty()) {
            FileInfo fileInfo = new FileInfo();
            fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
            this.fileInfoMapper.updateFileDelFlagBatch(fileInfo, userId, delFileSubFloderFileIdList, null, FileDelFlagEnums.DEL.getFlag());
        }

        //将前端选中的文件更新为正常, 且放置根目录下
        List<String> delFileIdList = Arrays.asList(fileIdArray);
        FileInfo fileInfo = new FileInfo();
        fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfo.setFilePid(Constants.ZERO_STR);
        fileInfo.setLastUpdateTime(new Date());
        this.fileInfoMapper.updateFileDelFlagBatch(fileInfo, userId, null, delFileIdList, FileDelFlagEnums.RECYCLE.getFlag());

        //前端所选文件重命名
        for (FileInfo item : fileInfoList) {
            if (rootFileMap.get(item.getFileName()) != null) {    //文件名已存在，对还原文件进行重命名
                FileInfo update = new FileInfo();
                update.setFileName(StringTools.rename(item.getFileName()));
                this.fileInfoMapper.updateByFileIdAndUserId(userId, item.getFileId(), update);
            }
        }
    }


    /**
     * 彻底删除文件（从数据库中删除）
     * @param userId
     * @param fileIds
     * @param adminOp   是否为管理员执行此方法
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delFileBatch(String userId, String fileIds, Boolean adminOp) {
        String[] fileIdArray = fileIds.split(",");
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFileIdArray(fileIdArray);
        query.setDelFlag(FileDelFlagEnums.RECYCLE.getFlag());
        List<FileInfo> fileInfoList = this.fileInfoMapper.selectFileList(query);

        //遍历集合，递归收集需要彻底删除的所有子目录
        List<String> delFileSubFloderFileIdList = new ArrayList<>();
        for (FileInfo fileInfo : fileInfoList) {
            if (FileFolderTypeEnums.FOLDER.getType().equals(fileInfo.getFolderType())) {
                findAllSubFolderFileList(delFileSubFloderFileIdList, userId, fileInfo.getFileId(), FileDelFlagEnums.DEL.getFlag());
            }
        }

        //将收集到的子目录进行删除（连同目录下的子文件）
        if (!delFileSubFloderFileIdList.isEmpty()) {
            this.fileInfoMapper.delFileBatch(userId, delFileSubFloderFileIdList, null, adminOp ? null : FileDelFlagEnums.DEL.getFlag());
        }

        //将前端选中的文件进行删除
        List<String> delFileIdList = Arrays.asList(fileIdArray);
        this.fileInfoMapper.delFileBatch(userId, null, delFileIdList, adminOp ? null : FileDelFlagEnums.RECYCLE.getFlag());

        //数据库用户使用空间更新
        Long useSpace = this.fileInfoMapper.selectUseSpace(userId);   //重新计算已使用的空间
        UserInfo userInfo = new UserInfo();
        userInfo.setUseSpace(useSpace);
        this.userInfoMapper.updateByUserId(userInfo, userId);

        //redis用户使用空间更新
        UserSpaceDto spaceDto = (UserSpaceDto) redisUtils.get(Constants.REDIS_KEY_USER_SPACE_USE + userId);
        spaceDto.setUseSpace(useSpace);
        redisUtils.set(Constants.REDIS_KEY_USER_SPACE_USE + userId, spaceDto);
    }


    /**
     * 查询文件，且关联user表查询用户名
     * @param query
     * @return
     */
    @Override
    public PaginationResultVo findFileAndNickName(FileInfoQuery query) {
        PaginationResultVo resultVo = new PaginationResultVo();
        resultVo.setPageSize(query.getPageSize());      //每页显示数据项数
        resultVo.setPageNo(query.getPageNo());          //页码

        //计算总记录数
        int totalCount = fileInfoMapper.findTotalCount(query);
        resultVo.setTotalCount(totalCount);

        //查询数据集
        int start = (query.getPageNo() - 1) * query.getPageSize();  //计算起始位置
        query.setStart(start);
        List<FileInfo> list = fileInfoMapper.findFileAndNickName(query);
        resultVo.setList(list);

        //计算总页数
        int pageTotal = totalCount % query.getPageSize() == 0 ?
                totalCount /query.getPageSize() : (totalCount / query.getPageSize()) + 1;
        resultVo.setPageTotal(pageTotal);

        return resultVo;
    }


    /**
     * 判断 要进入的目录 是否是 分享文件 的子文件或孙文件（不是则抛异常）
     * @param rootFilePid  分享文件的fileId
     * @param userId       用户Id
     * @param fileId       要进入的目录fileId
     */
    @Override
    public void checkRootFilePid(String rootFilePid, String userId, String fileId) {
        if (StringTools.isEmpty(fileId)) {
            throw new MyException(ResponseCodeEnum.CODE_605.getCode(), ResponseCodeEnum.CODE_605.getMsg());
        }
        //要进入的目录为分享文件本身，则返回
        if (rootFilePid.equals(fileId)) {
            return;
        }
        checkFilePid(rootFilePid, userId, fileId);
    }


    /**
     * 将分享文件保存到网盘
     * @param shareRootFilePid  分享文件的fileId
     * @param shareFileIds      选中的文件的fileId（分享的可能是一个目录，里面有多个文件）
     * @param myFolderId        保存的目的地
     * @param shareUserId       分享文件的用户
     * @param currentUserId     保存的用户
     */
    @Override
    public void saveShare(String shareRootFilePid, String shareFileIds, String myFolderId, String shareUserId, String currentUserId) {

        String[] shareFileIdArray = shareFileIds.split(",");

        //查询目的地目录下的所有文件
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(currentUserId);
        query.setFilePid(myFolderId);
        List<FileInfo> currentFileList = fileInfoMapper.selectFileList(query);
        //将 list 转换为以 fileName 为key的Map集合（如果有两个重名的情况选第二个）
        Map<String, FileInfo> currentFileMap = currentFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (data1, data2) -> data2));

        //选中的文件
        query = new FileInfoQuery();
        query.setUserId(shareUserId);
        query.setFileIdArray(shareFileIdArray);
        List<FileInfo> shareFileList = fileInfoMapper.selectFileList(query);

        List<FileInfo> copyFileList = new ArrayList<>();  //用于收集的文件集
        Date curDate = new Date();

        //判断 目的地目录中 是否有与 选中的文件 同名的文件
        for (FileInfo item : shareFileList) {
            FileInfo haveFile = currentFileMap.get(item.getFileName());
            if (haveFile != null) {
                item.setFileName(StringTools.rename(item.getFileName()));   //重命名
            }
            //收集复制文件（包含需复制文件下的子文件）
            findAllSubFile(copyFileList, item, shareUserId, currentUserId, curDate, myFolderId);
        }

        this.fileInfoMapper.insertBatch(copyFileList);  //持久化

    }


    /**
     * 复制文件（包含需复制文件下的子文件）
     * @param copyFileList    准备好持久化的文件集
     * @param fileInfo        要复制的文件
     * @param sourceUserId    要复制的文件的所属用户
     * @param currentUserId   复制的目标用户
     * @param curDate         时间
     * @param newFilePid      复制的目标目录
     */
    private void findAllSubFile(List<FileInfo> copyFileList, FileInfo fileInfo, String sourceUserId, String currentUserId, Date curDate, String newFilePid) {
        //提取原id值
        String sourceFileId = fileInfo.getFileId();

        //修改字段
        fileInfo.setCreateTime(curDate);     //创建时间
        fileInfo.setLastUpdateTime(curDate); //最近一次更新时间
        fileInfo.setFilePid(newFilePid);     //父Id
        fileInfo.setUserId(currentUserId);   //用户Id
        fileInfo.setFileId(StringTools.getRandomString(10));  //新的文件id
        copyFileList.add(fileInfo);          //将准备好的fileInfo添加入集合

        //判断需复制的文件是否为目录，是则递归收集子文件
        if (fileInfo.getFolderType().equals(FileFolderTypeEnums.FOLDER.getType())) {
            FileInfoQuery query = new FileInfoQuery();
            query.setUserId(sourceUserId);
            query.setFilePid(sourceFileId);
            List<FileInfo> sourceFileList = fileInfoMapper.selectFileList(query);  //复制文件下的子文件
            for (FileInfo item : sourceFileList) {
                findAllSubFile(copyFileList, item, sourceUserId, currentUserId, curDate, newFilePid);
            }
        }
    }



//-----------------------------------------------------------------------------------------------------


    //根据条件判断是否已有该文件名，有则给返回新文件名
    private String autoRename(String filePid, String userId, String fileName) {
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setUserId(userId);
        fileInfoQuery.setFilePid(filePid);
        fileInfoQuery.setFileName(fileName);
        fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag());

        Integer count = fileInfoMapper.selectFileCount(fileInfoQuery);
        if (count > 0) {
            fileName = StringTools.rename(fileName); //给文件名末尾加上随机数
        }
        return fileName;
    }

    //更新用户网盘空间使用情况 (参数fileSpace：新上传的文件大小)
    private void updateUserSpace(SessionWebUserDto webUserDto, Long fileSpace) {
        //更新数据库
        Integer count = userInfoMapper.updateUserSpace(webUserDto.getUserId(), fileSpace, null);
        if (count == 0) {
            ResponseCodeEnum r = ResponseCodeEnum.CODE_904;
            throw new MyException(r.getCode(), r.getMsg());
        }
        //更新redis
        String userId = webUserDto.getUserId();
        UserSpaceDto spaceDto = (UserSpaceDto) redisUtils.get(Constants.REDIS_KEY_USER_SPACE_USE + userId);
        spaceDto.setUseSpace(spaceDto.getUseSpace() + fileSpace);               //用户已使用空间
        redisUtils.set(Constants.REDIS_KEY_USER_SPACE_USE + userId, spaceDto); //更新redis
    }

    //更新 暂存目录内的分片总和大小
    private void saveFileTempSize(String userId, String fileId, Long fileSize) {
        Long currentSize = getFileTempSize(userId, fileId); //调用自定义方法获得 暂存目录内的分片总和大小
        //redis更新
        redisUtils.setex(
                Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId,
                currentSize + fileSize,
                60 * 60); //存活时间一分钟
    }

    //查询暂存目录内的分片总和大小
    private Long getFileTempSize(String userId, String fileId) {
        //从redis中查询已上传至暂存目录的分片总和大小
        Object sizeObj = redisUtils.get(Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId);
        if (sizeObj == null) {
            return 0L;
        }

        //转换为Long
        if (sizeObj instanceof  Integer) {
            return ((Integer) sizeObj).longValue();
        } else if (sizeObj instanceof Long) {
            return (Long) sizeObj;
        }

        return 0L;
    }

    //合并文件
    @Async   //异步
    public void transferFile(String fileId, SessionWebUserDto webUserDto) {
        Boolean transferSuccess = true;        //是否转码成功
        String targetFilePath = null;          //目标文件路径（永久目录）
        String cover = null;                   //封面
        FileTypeEnums fileTypeEnum = null;     //合并文件的类型
        FileInfo fileInfo = fileInfoMapper.findByFileIdAndUserId(fileId, webUserDto.getUserId()); //根据文件id和用户id查询
        try {
            if (fileInfo == null || !FileStatusEnums.TRANSFER.getStatus().equals(fileInfo.getStatus())) {
                return;
            }
            //暂存临时目录
            String tempFolderName = projectFolder + "/temp/";
            String currentUserFolderName = webUserDto.getUserId() + fileId;
            File fileFolder = new File(tempFolderName + currentUserFolderName);

            String fileSuffix = StringTools.getFileSuffix(fileInfo.getFileName()); //获取后缀名
            String month = DateUtil.toYYYYMM(new Date());                          //将date类型转换成yyyymm格式

            //目标目录
            String targetFolderName = projectFolder + "/file/";
            // /folder/ ToDo 校验路径
            File targetFolder = new File(targetFolderName + "/" + month);
            if (!targetFolder.exists()) {
                targetFolder.mkdirs();    //目录不存在则创建
            }
            String realFileName = currentUserFolderName + fileSuffix;     //真实的文件名
            targetFilePath = targetFolder.getPath() + "/" + realFileName;

            //合并文件
            union(fileFolder.getPath(), targetFilePath, fileInfo.getFileName(), true);

            //生成缩略图，用于预览
            fileTypeEnum = FileTypeEnums.getFileTypeBySuffix(fileSuffix); //根据文件后缀匹配类型
            if (FileTypeEnums.VIDEO == fileTypeEnum) {
                //视频文件切割（用于在线预览）
                cutFile4Video(fileId, targetFilePath);
                //视频生成缩略图（封面）
                cover = month + "/" + currentUserFolderName + ".png";
                String coverPath = targetFolderName + "/" + cover;
                ScaleFilter.createCover4Video(new File(targetFilePath), 150, new File(coverPath));
            } else if (FileTypeEnums.IMAGE == fileTypeEnum) {
                //生成缩略图（预览）
                cover = month + "/" + realFileName.replace(".", "_."); //将 . 替换为 _.
                String coverPath = targetFolderName + "/" + cover;
                Boolean created = ScaleFilter.createThumbnailWidthFFmpeg(new File(targetFilePath), 150, new File(coverPath), false);
                if (!created) {
                    FileUtils.copyFile(new File(targetFilePath), new File(coverPath)); //缩略图生成失败，则直接复制（前为源文件，后为目标文件）
                }
            }
        } catch (Exception e) {
            logger.error("文件转码失败，文件ID：{}，userId: {}", fileId, webUserDto.getUserId(), e);
            transferSuccess = false;
        } finally {
            FileInfo updateInfo = new FileInfo();
            updateInfo.setFileSize(new File(targetFilePath).length());   //更新文件大小
            updateInfo.setFileCover(cover);                              //更新封面
            updateInfo.setStatus(transferSuccess ? FileStatusEnums.USING.getStatus() : FileStatusEnums.TRANSFER_FAIL.getStatus());//更新文件转码状态
            fileInfoMapper.updateFileStatusWithOldStatus(fileId, webUserDto.getUserId(), FileStatusEnums.TRANSFER.getStatus(), updateInfo);
        }
    }

    //读写分片文件 参数delSource：是否删除暂存临时目录内的文件
    private void union(String dirPath, String toFilePath, String fileName, Boolean delSource) {
        //判断源文件（暂存临时目录）
        File dir = new File(dirPath);
        if (!dir.exists()) {
            ResponseCodeEnum r = ResponseCodeEnum.CODE_905;
            throw new MyException(r.getCode(), r.getMsg());
        }

        File[] fileList = dir.listFiles();      //获取暂存临时目录内所有的文件
        File targetFile = new File(toFilePath); //目标目录
        RandomAccessFile writeFile = null;
        try {
            writeFile = new RandomAccessFile(targetFile, "rw");
            byte[] b = new byte[1024 * 10];
            for (int i = 0; i < fileList.length; i++) {
                int len = -1;
                File chunkFile = new File(dirPath + "/" + i);     //选中分片
                RandomAccessFile readFile = null;
                try {
                    readFile = new RandomAccessFile(chunkFile, "r"); //读取分片
                    while ((len = readFile.read(b)) != -1) {
                        writeFile.write(b, 0, len);                     //写入分片
                    }
                } catch (Exception e) {
                    logger.error("合并分片失败", e);
                    throw new MyException(1001, "合并分片失败");
                } finally {
                    readFile.close();   //关闭资源
                }
            }
        } catch (Exception e) {
            logger.error("合并文件：{}失败", fileName, e);
            throw new MyException(1002, "合并文件: " + fileName + "--失败");
        } finally {
            if (writeFile != null) {
                try {
                    writeFile.close();  //关闭资源
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (delSource && dir.exists()) {
                try {
                    FileUtils.deleteDirectory(dir);   //删除源文件（暂存临时目录）
                } catch (Exception e) {
                    System.out.println("\n----------------------------------------400------------\n");
                    e.printStackTrace();
                }
            }
        }
    }

    //将视频文件（如：.mp4）先转换成index.ts文件，之后再生成索引文件.m3u8和切割.ts文件，最后删除index.ts文件
    //参数：videoFilePath -- 要切割的视频文件路径（/folder/202401/133212DH32SS64.mp4）
    private void cutFile4Video(String fileId, String videoFilePath) {
        //创建同名切片目录
        File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
        if (!tsFolder.exists()) {
            tsFolder.mkdirs();
        }
        final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
        final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";
        String tsPath = tsFolder + "/" + Constants.TS_NAME;

        //生成.ts
        String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
        ProcessUtils.executeCommand(cmd, false);
        //生成索引文件.m3u8和切片.ts
        cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath(), fileId);
        ProcessUtils.executeCommand(cmd, false);
        //删除index.ts
        new File(tsPath).delete();
    }

    //校验文件名称是否已存在
    private void checkFileName(String userId, String filePid, String fileName, Integer folderType) {
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setUserId(userId);
        fileInfoQuery.setFilePid(filePid);
        fileInfoQuery.setFileName(fileName);
        fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfoQuery.setFolderType(folderType);
        Integer count = fileInfoMapper.selectFileCount(fileInfoQuery);
        if (count > 0) {
            throw new MyException(599, "此目录下已存在同名文件，请修改名称");
        }
    }

    //从fileId开始，遍历找出所有目录
    private void findAllSubFolderFileList(List<String> fileIdList, String userId, String fileId, Integer delFlag) {
        fileIdList.add(fileId);                     //将待删除的文件添加进集合中
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFilePid(fileId);
        query.setDelFlag(delFlag);
        query.setFolderType(FileFolderTypeEnums.FOLDER.getType());
        List<FileInfo> fileInfoList = this.fileInfoMapper.selectFileList(query); //查询待删除文件下的其它目录
        for (FileInfo fileInfo : fileInfoList) {
            //递归：
            findAllSubFolderFileList(fileIdList, userId, fileInfo.getFileId(), delFlag);
        }
    }

    //从fileId开始，根据Pid向上递归查找，直到找出rootFilePid
    private void checkFilePid(String rootFilePid, String userId, String fileId) {
        FileInfo fileInfo = this.fileInfoMapper.findByFileIdAndUserId(fileId, userId);
        //此文件不存在
        if (fileInfo == null) {
            throw new MyException(ResponseCodeEnum.CODE_605.getCode(), ResponseCodeEnum.CODE_605.getMsg());
        }
        //已到达根目录，无法继续向上递归查找，证明 “fileId” 不是 “rootFilePid” 的子文件或孙文件
        if (Constants.ZERO_STR.equals(fileInfo.getFilePid())) {
            throw new MyException(ResponseCodeEnum.CODE_605.getCode(), ResponseCodeEnum.CODE_605.getMsg());
        }
        // fileId 是 rootFilePid 的子文件或孙文件
        if (fileInfo.getFilePid().equals(rootFilePid)) {
            return;
        }
        checkFilePid(rootFilePid, userId, fileInfo.getFilePid()); //继续向上递归查找，直至找到rootFilePid或根目录
    }
}
